/* 
Scenic Graphics Library
Copyright (C) 2007 Jouni Tulkki

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USApackage scenic;
*/

#include "common.h"
#include "Graphics.h"
#include "Util.h"

#define INT_PTR ptrdiff_t
#include <GL/glext.h>


// #define DEBUG_DEVICE


struct FormatInfo
{
	GLenum internalFormat;
	GLenum format;
	GLenum type;
	int size;
};


using namespace Scenic::Renderer;


CriticalRenderingSection Scenic::Renderer::renderingLock;


static bool extensionsInited = false;
static PFNGLGENFRAMEBUFFERSEXTPROC glGenFramebuffersEXT;
static PFNGLBINDFRAMEBUFFEREXTPROC glBindFramebufferEXT;
static PFNGLFRAMEBUFFERTEXTURE2DEXTPROC glFramebufferTexture2DEXT;
static PFNGLGENRENDERBUFFERSEXTPROC glGenRenderbuffersEXT;
static PFNGLBINDRENDERBUFFEREXTPROC glBindRenderbufferEXT;
static PFNGLRENDERBUFFERSTORAGEEXTPROC glRenderbufferStorageEXT;
static PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC glFramebufferRenderbufferEXT;
static PFNGLDELETEFRAMEBUFFERSEXTPROC glDeleteFramebuffersEXT;
static PFNGLDELETERENDERBUFFERSEXTPROC glDeleteRenderbuffersEXT;

static FormatInfo formats[] = 
{
	{GL_RGBA, GL_BGRA, GL_UNSIGNED_BYTE, 4},
	{GL_RGB, GL_BGR, GL_UNSIGNED_BYTE, 3},
	{GL_LUMINANCE, GL_LUMINANCE, GL_UNSIGNED_BYTE, 1},
	{GL_ALPHA, GL_ALPHA, GL_UNSIGNED_BYTE, 1},
	{(GLenum)0, (GLenum)0, (GLenum)0, 1},
	{(GLenum)0, (GLenum)0, (GLenum)0, 4},
	{(GLenum)0, (GLenum)0, (GLenum)0, 8},
	{(GLenum)0, (GLenum)0, (GLenum)0, 16},
	{(GLenum)0, (GLenum)0, (GLenum)0, 4},
	{(GLenum)0, (GLenum)0, (GLenum)0, 8},

	{0, 0, GL_UNSIGNED_BYTE, 1}
};

static void initExtensions()
{
	if(extensionsInited)
		return;
	glGenFramebuffersEXT = (PFNGLGENFRAMEBUFFERSEXTPROC)glXGetProcAddress((GLubyte*)"glGenFramebuffersEXT");
	glBindFramebufferEXT = (PFNGLBINDFRAMEBUFFEREXTPROC)glXGetProcAddress((GLubyte*)"glBindFramebufferEXT");
	glFramebufferTexture2DEXT = (PFNGLFRAMEBUFFERTEXTURE2DEXTPROC)glXGetProcAddress((GLubyte*)"glFramebufferTexture2DEXT");
	glGenRenderbuffersEXT = (PFNGLGENRENDERBUFFERSEXTPROC)glXGetProcAddress((GLubyte*)"glGenRenderbuffersEXT");
	glBindRenderbufferEXT = (PFNGLBINDRENDERBUFFEREXTPROC)glXGetProcAddress((GLubyte*)"glBindRenderbufferEXT");
	glRenderbufferStorageEXT = (PFNGLRENDERBUFFERSTORAGEEXTPROC)glXGetProcAddress((GLubyte*)"glRenderbufferStorageEXT");
	glFramebufferRenderbufferEXT = (PFNGLFRAMEBUFFERRENDERBUFFEREXTPROC)glXGetProcAddress((GLubyte*)"glFramebufferRenderbufferEXT");
	glDeleteFramebuffersEXT = (PFNGLDELETEFRAMEBUFFERSEXTPROC)glXGetProcAddress((GLubyte*)"glDeleteFramebuffersEXT");
	glDeleteRenderbuffersEXT = (PFNGLDELETERENDERBUFFERSEXTPROC)glXGetProcAddress((GLubyte*)"glDeleteRenderbuffersEXT");
	extensionsInited = true;
}

//
// Device
//

static Surface *currentSurface = 0;
static GLXContext shareContext = 0;
Display *display = 0;
static ResourcePool devicePool("Device");
static Device *currentDevice = 0;


XVisualInfo *getVisualInfo(Display *display)
{
	int attrib[] = 
	{
		GLX_DOUBLEBUFFER, 
		GLX_RGBA,
		GLX_RED_SIZE, 1,
		GLX_GREEN_SIZE, 1,
		GLX_BLUE_SIZE, 1,
//		GLX_ALPHA_SIZE, 8,
		GLX_DEPTH_SIZE, 1,
		None
	};
	
	return glXChooseVisual(display, DefaultScreen(display), attrib);
}

Capabilities &Device::getCaps()
{
	static Capabilities caps;
	int i;
	
	caps.nonPow2Conditional = true;
	for(i = 0; i < FMT_COUNT; i++)
	{
		caps.textureFormats[i] = false;
	}
	caps.textureFormats[FMT_A8R8G8B8] = true;
	caps.textureFormats[FMT_R8G8B8] = true;
	caps.textureFormats[FMT_L8] = false;
	caps.textureFormats[FMT_A8] = false;
	caps.textureFormats[FMT_AL8] = false;
//	caps.maxTextureWidth = 2048;
//	caps.maxTextureHeight = 2048;
	caps.maxTextureWidth = 4096;
	caps.maxTextureHeight = 4096;
	
	return caps;
}

Device::Device(Window window)
	: Resource(&devicePool)
{
	if(!::display)
		::display = XOpenDisplay(0);

	initExtensions();
	this->display = ::display;

	windowSurface = 0;
	windowSurface = new Surface(this, window);
	drawSurface = windowSurface;
	drawSurface->addRef();
	
	makeCurrent();
	checkCaps();
	initState();
	for(int i = 0; i < 2; i++)
	{
		textureModulation[i] = 1;
		textureTarget[i] = -1;
	}
}

Device::~Device()
{
	drawSurface->release();
	windowSurface->release();
	currentSurface = 0;
	if(currentDevice == this)
		currentDevice = 0;
}

Device *Device::getDefaultDevice()
{
	DynArray<Resource*> &devices = devicePool.getAll();

	if(devices.size > 0)
		return (Device*)devices[0];
	return NULL;
}

void Device::checkCaps()
{
	caps.nonPow2Conditional = true;
	caps.textureFormats[FMT_A8R8G8B8] = true;
	caps.textureFormats[FMT_R8G8B8] = true;
	caps.textureFormats[FMT_L8] = false;
	caps.textureFormats[FMT_A8] = false;
	caps.textureFormats[FMT_AL8] = false;

	printf("\n---DEVICE CAPABILITIES---\n");
	printf("Conditional non power of 2 textures: %s\n", 
		caps.nonPow2Conditional ? "yes" : "no");
	printf("R8G8B8 texture format: %s\n", caps.textureFormats[FMT_R8G8B8] ? "yes" : "no");
	printf("L8 texture format: %s\n", caps.textureFormats[FMT_L8] ? "yes" : "no");
	printf("A8 texture format: %s\n", caps.textureFormats[FMT_A8] ? "yes" : "no");
	printf("\n");
}

void Device::initState()
{
	glDisable(GL_LIGHTING);
	glDisable(GL_CULL_FACE);
	glEnable(GL_BLEND);
    glDisable(GL_DEPTH_TEST);
	glDisable(GL_SCISSOR_TEST);
}

void Device::makeCurrent()
{
	while(XPending(display) > 0)
	{
		XEvent event;
	
		XNextEvent(display, &event);
		printf("Received XEvent (type=%d)\n", event.type);
	}
	Surface *drawSurface = windowSurface;
	if(currentSurface != drawSurface)
	{
		if(glXMakeCurrent(display, drawSurface->drawable, drawSurface->context) == False)
			printf("glXMakeCurrent failed\n");
		currentSurface = drawSurface;
	}
	currentDevice = this;
}

void Device::reset()
{
	makeCurrent();
	initState();
}

void Device::initializeViewport(int width, int height)
{
	makeCurrent();
	glViewport(0, 0, width, height);
	glLoadIdentity();
	if(drawSurface->isWindow)
		glOrtho(0, width, height, 0, 0, 1);
	else
		glOrtho(0, width, 0, height, 0, 1);
}

bool Device::beginScene()
{
	makeCurrent();
	return true;
}

void Device::endScene()
{
}

void Device::present()
{
	makeCurrent();
	glXSwapBuffers(display, windowSurface->drawable);
}

void Device::clear(const RGBAFloat &color, SizeRect *rect)
{
	makeCurrent();
	glClearColor(color.red, color.green, color.blue, color.alpha);
	if(rect)
	{
		SurfaceInfo info = drawSurface->getInfo();
		
		glScissor(rect->x, info.height - (rect->y + rect->height), rect->width, rect->height);
		glEnable(GL_SCISSOR_TEST);
		glClear(GL_COLOR_BUFFER_BIT);
		glDisable(GL_SCISSOR_TEST);
	}
	else 
	{
		glClear(GL_COLOR_BUFFER_BIT);
	}
}

void Device::setRenderTarget(Surface *surface)
{
	if(drawSurface)
	{
		drawSurface->endRender();
		drawSurface->release();
	}
	drawSurface = surface;
	if(drawSurface)
	{
		drawSurface->addRef();
		drawSurface->beginRender();
//		makeCurrent();
//		initState();
	}
}

Surface *Device::getRenderTarget()
{
	drawSurface->addRef();
	return drawSurface;
}

void Device::setTexture(int stage, BaseTexture *texture)
{
	GLdouble m[4][4];

	glActiveTextureARB(GL_TEXTURE0_ARB + stage);
	if(textureTarget[stage] != -1)
		glBindTexture(textureTarget[stage], 0);
	glDisable(GL_TEXTURE_2D);
	glDisable(GL_TEXTURE_3D);
	glDisable(GL_TEXTURE_RECTANGLE_EXT);
	if(texture)
	{
//		printf("Bind texture %d %d %s\n", stage, texture->texture, texture->target == GL_TEXTURE_2D ? "POWOF2" : "");
		glBindTexture(texture->target, texture->texture);
		glEnable(texture->target);
		glMatrixMode(GL_TEXTURE);
		if(texture->target == GL_TEXTURE_RECTANGLE_EXT)
		{
			Texture *tex = (Texture*)texture;
			
			for(int r = 0; r < 4; r++)
				for(int c = 0; c < 4; c++)
					m[r][c] = 0.0;
			m[0][0] = tex->width;
			m[1][1] = tex->height;
			m[2][2] = 1.0;
			m[3][3] = 1.0;
			glLoadMatrixd(&m[0][0]);
		}
		else
		{
			glLoadIdentity();
		}
		glMatrixMode(GL_MODELVIEW);
		textureTarget[stage] = texture->target;
	}
	else
	{
		textureModulation[stage] = 1;
	}
}

void Device::setSamplerState(int stage, SamplerStateType type, int value)
{
	makeCurrent();
	glActiveTextureARB(GL_TEXTURE0_ARB + stage);
	if(type != -1)
	{
		glTexParameteri(GL_TEXTURE_2D, (GLenum)type, (GLint)value);
		glTexParameteri(GL_TEXTURE_RECTANGLE_EXT, (GLenum)type, (GLint)value);
	}
}

void Device::setSamplerStates(int stage, Filter filter, Address address)
{
	setSamplerState(stage, SAMPLER_ADDRESSU, address);
	setSamplerState(stage, SAMPLER_ADDRESSV, address);
	setSamplerState(stage, SAMPLER_MAG_FILTER, filter);
	setSamplerState(stage, SAMPLER_MIN_FILTER, filter);
}

void Device::setTextureStageModulation(int stage, int modulation)
{
	textureModulation[stage] = modulation;
}

int Device::getTotalModulation()
{
	return textureModulation[0] * textureModulation[1];
}

void Device::setState(State state, int value)
{
	makeCurrent();
	if(state == STATE_BLENDOP)
		glBlendEquation((GLenum)value);
}

void Device::setBlendFunc(BlendFactor sfactor, BlendFactor dfactor)
{
	makeCurrent();
	glBlendFunc((GLenum)sfactor, (GLenum)dfactor);
}

int Device::getVisualId(int screen)
{
	if(!::display)
		::display = XOpenDisplay(0);
		
	XVisualInfo *visinfo = getVisualInfo(::display);
	int id = visinfo->visual->visualid;
	
	XFree(visinfo);
	return id;
}

int Device::getFormatSize(Format format)
{
	return formats[format].size;
}

Device *Device::getShareDevice()
{
	DynArray<Resource*> &devices = devicePool.getAll();
	
	if(currentDevice)
		return currentDevice;
	for(int i = 0; i < devices.size; i++)
	{
		Device *d = (Device*)devices[i];
		
		if(d && d->windowSurface)
			return d;
	}
	
	return 0;
}

//
// Surface
//

Surface::Surface(Device *device, Window window)
{
	Device *shareDevice = Device::getShareDevice();
	GLXContext shareList = shareDevice ? shareDevice->windowSurface->context : 0;
	
	this->device = device;
	refCount = 1;
	drawable = window;
	texture = 0;
	isWindow = true;
	
	visinfo = getVisualInfo(device->display);
	printf("VisualID: %d\n", visinfo->visual->visualid);
	context = glXCreateContext(device->display, visinfo, shareList, True);
	if(!context)
	{
		printf("Creation of GLXContext failed\n");
		return;
	}
}

Surface::Surface(Texture *texture, int level)
{
	refCount = 1;
	device = 0;
	context = 0;
	this->texture = texture;
	this->level = level;
	isWindow = false;
	
	if(texture)
	{
		format = texture->format;
		surfaceFormat = texture->textureFormat;
		texture->addRef();
	}
}

Surface::~Surface()
{
	if(refCount != 0)
		printf("Surface reference count %d != 0\n", refCount);
	if(context)
		glXDestroyContext(device->display, context);
	if(texture)
		texture->release();
		
}

void Surface::release()
{
	refCount--;
	if(refCount == 0)
		delete this;
}

void Surface::addRef()
{
	refCount++;
}


void Surface::beginRender()
{
	if(texture)
	{
		context = 0;
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, texture->framebuffer);
	}
}

void Surface::endRender()
{
	if(texture)
	{
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
	}
}

SurfaceInfo Surface::getInfo()
{
	SurfaceInfo info;
	
	if(isWindow)
	{
		XWindowAttributes a;
		
		XGetWindowAttributes(device->display, drawable, &a);
		info.width = a.width;
		info.height = a.height;
	}
	else
	{
		info.width = texture->width;
		info.height = texture->height;
	}

	return info;
}

//
// Texture
//


static ResourcePool texturePool("BaseTexture");

BaseTexture::BaseTexture()
	: Resource(&texturePool)
{
}


bool isPowerOfTwo(int a)
{
	while(a > 1)
	{
		if(a & 1)
			return false;
		a >>= 1;
	}
	return true;
}

Texture::Texture(int width, int height, Format format, int usage, int levels)
{
	this->format = format;
	this->width = width;
	this->height = height;
	this->levels = levels;
	this->usage = usage;
	framebuffer = 0;
	target = (isPowerOfTwo(width) && isPowerOfTwo(height)) ? GL_TEXTURE_2D : GL_TEXTURE_RECTANGLE_EXT;
	// For some reason GL_TEXTURE_2D does not work for render targets
	if(usage & USAGE_RENDER_TARGET)
		target = GL_TEXTURE_RECTANGLE_EXT;
	createTexture();
}

void Texture::createTexture()
{
	Device *device = Device::getShareDevice();
	
	if(!device)
		return;
		
	if(device->caps.textureFormats[format])
		textureFormat = format;
	else
		textureFormat = FMT_A8R8G8B8;

	device->makeCurrent();
	
	if(usage & USAGE_RENDER_TARGET)
	{
		glGenTextures(1, &texture);
		glBindTexture(target, texture);
		glTexImage2D(target, 0, formats[textureFormat].internalFormat,
			width, height, 0, formats[textureFormat].format, formats[textureFormat].type,
			NULL);
			
		glGenFramebuffersEXT(1, &framebuffer);
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, framebuffer);		
		glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT,
			target, texture, 0);
		glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);		
	}
	else 
	{
		framebuffer = 0;
		glGenTextures(1, &texture);
		glBindTexture(target, texture);
		glTexImage2D(target, 0, formats[textureFormat].internalFormat,
			width, height, 0, formats[textureFormat].format, formats[textureFormat].type,
			NULL);
	}
	
#ifdef DEBUG_DEVICE
	printf("New texture %d (width=%d, height=%d, format=%d, textureFormat=%d usage=%d)\n", 
		texture, width, height, format, textureFormat, usage);	
#endif
}

Texture::~Texture()
{
	Device *device = Device::getShareDevice();
	
#ifdef DEBUG_DEVICE
	printf("Delete texture %d\n", texture);
#endif
	device->makeCurrent();
	if(framebuffer)
		glDeleteFramebuffersEXT(1, &framebuffer);
	glDeleteTextures(1, &texture);
}
/*
void Texture::addRef()
{
	refCount++;
}

void Texture::release(bool isAsync)
{
	refCount--;
	if(refCount == 0)
	{
		if(isAsync)
			texturesToDelete.add(this);
		else
			delete this;
	}
}
*/
bool Texture::contentsLost() 
{
	return false;
}

Surface *Texture::getSurface(int level)
{
	return new Surface(this, level);
}

void Texture::load()
{
	if(texture)
		return;
	createTexture();
	writeTexture(0, 0, 0, width, height, localData, width, format);
	delete localData;
}

void Texture::unload()
{
	if(!texture)
		return;
}

void Texture::write(int level, int x, int y, int width, int height, void *data, int pitch, Format format)
{
	if(texture)
	{
		writeTexture(level, x, y, width, height, data, pitch, format);
	}
	else
	{
		int size = formats[textureFormat].size;
		char *src = (char*)data;
		int srcPitch = pitch;
		char *dst = localData + (this->width * y + x) * size;
		int dstPitch = this->width * size;

		convert(src, srcPitch, format, 
			dst, dstPitch, textureFormat,
			width, height);
	}
}

void Texture::writeTexture(int level, int x, int y, int width, int height, void *data, int pitch, Format format)
{
	Device *device = Device::getShareDevice();
	
	device->makeCurrent();
	glBindTexture(target, texture);
	glPixelStorei(GL_UNPACK_ALIGNMENT, 1);	
	if(format == textureFormat)
	{
		int size = formats[format].size;

		glPixelStorei(GL_UNPACK_ROW_LENGTH, pitch / size);
		glTexSubImage2D(target, 0, x, y, width, height,
			formats[textureFormat].format, formats[textureFormat].type, 
			data);
		glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
	}
	else
	{
		int size = formats[format].size;
		char *dest = new char[width * height * formats[textureFormat].size];
		
		convert((char*)data, pitch, format, 
			dest, width * formats[textureFormat].size, textureFormat, width, height); 
		glTexSubImage2D(target, 0, x, y, width, height,
			formats[textureFormat].format, formats[textureFormat].type, 
			dest);
		delete dest;
	}
	glBindTexture(target, 0);
}

void Texture::read(int level, int x, int y, int width, int height, void *data, int pitch, Format format)
{
	Device *device = Device::getShareDevice();
	Surface *oldSurface;
	Surface *surface;
	
	if(!(usage & USAGE_RENDER_TARGET)) {
		printf("Only render targets can be read.\n");
		return;
	}
	device->makeCurrent();
	oldSurface = device->getRenderTarget();
	surface = getSurface(0);
	device->setRenderTarget(surface);
	
	glPixelStorei(GL_PACK_ALIGNMENT, 1);	
	if(format == textureFormat)
	{
		int size = formats[format].size;

		glPixelStorei(GL_PACK_ROW_LENGTH, pitch / size);
		glReadPixels(x, y, width, height,
			formats[textureFormat].format, formats[textureFormat].type, 
			data);
		glPixelStorei(GL_PACK_ROW_LENGTH, 0);
	}
	else
	{
		int size = formats[format].size;
		char *dest = new char[width * height * formats[textureFormat].size];
		
		glReadPixels(x, y, width, height,
			formats[textureFormat].format, formats[textureFormat].type, 
			dest);
		convert(dest, width * formats[textureFormat].size, textureFormat, 
			(char*)data, pitch, format, 
			width, height); 
		delete dest;
	}
	
	
	device->setRenderTarget(oldSurface);
	oldSurface->release();
	surface->release();
}

void CriticalRenderingSection::onEnter()
{
}

void CriticalRenderingSection::onLeave()
{
	if(glXMakeCurrent(display, None, NULL) == False)
		printf("glXMakeCurrent NULL failed\n");
	currentSurface = NULL;
}
